/*
 * Copyright 2010 Biz-e (info@biz-e.nl)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package nl.biz_e.appengine.lib.server.objectify;

import java.util.List;
import java.util.Map;

import com.googlecode.objectify.Key;
import com.googlecode.objectify.Objectify;
import com.googlecode.objectify.Query;
import com.googlecode.objectify.helper.DAOBase;

/**
 * Typesafe base DAO for use with the Objectify library.<br/>
 * This Base DAO is made abstract because you MUST register the entity objects
 * in a static method before you can use them.<br/>
 * Also, the entity class must be registered in the DAO itself, because it is
 * needed for certain queries.<br/>
 * <br/>
 * The most basic DAO implementation would look like this:
 * 
 * <pre>
 * public class UserDao extends AbstractBaseDao<MyEntity> {
 *   static {
 *     ObjectifyService.register(MyEntity.class);
 *   }
 *   
 *   public UserDao() {
 *     super(MyEntity.class);
 *   }
 * }
 * </pre>
 * 
 * @author Joost A. Bloemsma
 * @param <E>
 *            The class of the Entity this DAO handles
 */
public class AbstractBaseDao<E extends Object> extends DAOBase {
    private boolean classRegistered = false;
    /**
     * Holds a reference to the class definition of the entity
     */
    protected Class<E> entityClz;

    /**
     * Constructs a new AbstractBaseDao instance
     * 
     * @param clazz
     */
    protected AbstractBaseDao(Class<E> clazz) {
        super();
        this.registerEntityClass(clazz);
    }

    /**
     * Registers the entity class
     * 
     * @param clazz
     */
    protected void registerEntityClass(Class<E> clazz) {
        this.entityClz = clazz;
        classRegistered = true;
    }

    /**
     * Constructs a new AbstractBaseDao instance without registering the class
     * Only temporarily left in cause I don't want to change everything yet ;-)
     * 
     * @deprecated
     */
    // @Deprecated
    // protected AbstractBaseDao() {
    // super();
    // }

    /**
     * Persists the passed entity to the datastore.
     * 
     * @param obj
     * @return the persisted entity
     * @see Objectify#put(Object)
     */
    public Key<E> save(E obj) {
        return ofy().put(obj);
    }

    /**
     * Persists the passed entities to the datastore.
     * 
     * @param objs
     *            a collection of entities to persist
     * @return A map of {@link Key}-Entity pairs
     * @see Objectify#put(Object)
     */
    public Map<Key<E>, E> save(Iterable<E> objs) {
        return ofy().put(objs);
    }

    /**
     * Deletes the passed entity from the datastore
     * 
     * @param obj
     *            The entity to delete
     * @see Objectify#delete(Object)
     */
    public void delete(E obj) {
        ofy().delete(obj);
    }

    /**
     * Deletes the entity identified by the passed key
     * 
     * @param key
     * @see Objectify#delete(Object)
     */
    public void delete(Key<E> key) {
        ofy().delete(key);
    }

    /**
     * Returns the entity identified by the passed Key
     * 
     * @param key
     * @return the entity
     * @see Objectify#get(Key)
     */
    public E get(Key<E> key) {
        return ofy().get(key);
    }

    /**
     * Returns a {@link List} of all available {@link Key}s.
     * 
     * @return instance of {@link List}
     * @throws EntityClassNotRegisteredException
     */
    public List<Key<E>> getAllKeys() {
        if (!classRegistered)
            throw new EntityClassNotRegisteredException();
        return qry().listKeys();
    }

    /**
     * @param keys
     * @return a {@link Map} if Key, entity pairs
     * @see Objectify#get(Iterable)
     */
    public Map<Key<E>, E> get(Iterable<Key<E>> keys) {
        return ofy().get(keys);
    }

    /**
     * Returns a single result from the passed {@link Query}. The responsibility
     * to ensure the query only contains one result lies with the method
     * creating the query.
     * 
     * @param query
     * @return The (first) entity as a resutl from the query. Null if no result
     *         at all.
     * @throws IllegalArgumentException
     *             if the query results in more
     *             than one entity.
     */
    protected E getResultSingle(Query<E> query) {
        if (query.countAll() == 0)
            return null;
        if (query.countAll() > 1)
            throw new IllegalArgumentException("Too many results in query");
        return query.get();
    }

    /**
     * Easy access to a typesafe {@link Query} object. Convenient shorthand
     * for {@code ofy().query(entityClz)}
     * 
     * @return instance of {@link Query}
     */
    protected Query<E> qry() {
        if (!classRegistered)
            throw new EntityClassNotRegisteredException();
        return ofy().query(this.entityClz);
    }

    /**
     * Returns a {@link List} of all entities.<br/>
     * If only a list of {@link Key}s is needed, use {@link #getAllKeys()}.
     * 
     * @return A list of all entities
     */
    public List<E> getAll() {
        if (!classRegistered)
            throw new EntityClassNotRegisteredException();
        return qry().list();
    }

    /**
     * Returns a List of entities filtered by the passed condition/value
     * combination
     * 
     * @param propName
     * @param value
     * @return List
     */
    public List<E> find(String propName, Object value) {
        Query<E> q = qry();
        q.filter(propName, value);
        return q.list();
    }

    /**
     * Returns the first entity that matches the passed condition/value
     * combination,
     * even if more than one matches exist
     * 
     * @param propName
     * @param value
     * @return E
     */
    public E findFirst(String propName, Object value) {
        Query<E> q = qry();
        q.filter(propName, value);
        q.limit(1);
        return q.get();
    }

    /**
     * Returns the single entity that matches the condition/value combination.
     * 
     * @param propName
     * @param value
     * @return E
     * 
     * @throws IllegalArgumentException
     *             if the query results in more
     *             than one entity.
     */
    public E findSingle(String propName, Object value) {
        Query<E> q = qry();
        q.filter(propName, value);
        return getResultSingle(q);
    }

    /**
     * Runtimeexception thrown when a method is called that needs the
     * ClassDefinition of the entity, but it wasn't registered yet.
     * 
     * @author Joost A. Bloemsma
     */
    public static class EntityClassNotRegisteredException extends
            RuntimeException {
        /**
         * 
         */
        private static final long serialVersionUID = 1L;

        /**
         * Constructs a new EntityClassNotRegisteredException instance
         */
        public EntityClassNotRegisteredException() {
            super("EntityClass must be registered with DAO for this operation");
        }
    }
}
